package lapsnake.model.expr;

import gui.typeEditors.StringEditor;

import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyVetoException;
import java.beans.VetoableChangeListener;

import lapsnake.model.*;
import lapsnake.model.bounceback.StringNode;
import lapsnake.model.data.*;
import lapsnake.model.dnd.ActionNodePanel;
import lapsnake.py.ast.Value;
import lapsnake.ui.Fonts;
import lapsnake.util.Funky;

import org.python.antlr.ast.*;
import org.python.core.PyString;

/**Wrapper class with helper functions for Str nodes*/
public class LapStr extends LapExpression implements Value<String>, VetoableChangeListener {
	public enum StringDelimiter
	{
		TRIPLE_DOUBLE("\"\"\""),
		TRIPLE_SINGLE("'''"),
		SINGLE("'"),
		DOUBLE("\"");
		
		public final String delimiter;
		
		private StringDelimiter(String delimiter)
		{
			this.delimiter = delimiter;
		}
		@Override
		public String toString()
		{
			return delimiter;
		}
		public String sanitize(String s)
		{
			boolean allowNewLine = delimiter.length() > 1;
			if(!allowNewLine) //Actual newline characters can occur in triple-quoted strings. Otherwise they need to be represented with \n.
				s = s.replace("\n", "\\"+'n');//Note: line breaks are impossible in (non-triplequoted) raw strings. This is the closest alternative.
			
			//Note: RSyntaxArea isn't happy about "s in """string"s""", so we will escape all of them even if this isn't strictly necessary.
			String limChar = String.valueOf(delimiter.charAt(0));
			//Note: Quotes can still be escaped in raw strings, but the backslash will remain... Very strange, but there's no need for special code. 
			s = Funky.replaceAndEscape(s, limChar, limChar);
			
			//Note: Jython doesn't care about stray backslashes("u\ wot\\\ m8"), so we don't need to do anything about it.
			//We only need to worry about a backslash before the last delimiter, e.g. "hello \" is not valid.
			int backslashes = 0;
			for(int i = s.length()-1; i>= 0; i--)
			{
				if(s.charAt(i) == '\\')
					backslashes++;
				else
					break;
			}
			if((backslashes%2) == 1)
				s = s+'\\';
			//Also, note that this particular restriction also applies to raw strings, because the backslash will still escape the delimiter...
			return s;
		}
	}
	public final VetoableProperty<String> value = new VetoableProperty<>(this, String.class);
	public final VetoableProperty<StringDelimiter> type = new VetoableProperty<>(this, StringDelimiter.class);
	public final VetoableProperty<Boolean> unicodeFlag = new VetoableProperty<>(this, false);
	public final VetoableProperty<Boolean> rawFlag = new VetoableProperty<>(this, false);
	
	public LapStr(PyAST pyAST, ChildField<? super LapStr> parent, StringDelimiter type, String value) throws PropertyVetoException
	{
		super(pyAST, parent, null);
		this.type.setInternal(type);
		this.value.setInternal(value);
		setTextNodes(new StringNode(type.delimiter+type.sanitize(value)+type.delimiter));
	}
	
	public LapStr(PyAST pyAST, ChildField<? super LapStr> parent, Str node) throws PropertyVetoException {
		super(pyAST, parent, node);
		value.setInternal(((PyString) node.getInternalS()).asString());
		
		//This will return the string literal with delimiters. 'value' has no delimiters.
		String token = node.getToken().getText();
		final int offset;
		
		String lowToken = token.toLowerCase();
		if(lowToken.startsWith("ur") || lowToken.startsWith("ru"))
		{
			unicodeFlag.setInternal(true);
			rawFlag.setInternal(true);
			offset = 2;
		}
		else if(lowToken.startsWith("u"))
		{
			unicodeFlag.setInternal(true);
			offset = 1;
		}
		else if(lowToken.startsWith("r"))
		{
			rawFlag.setInternal(true);
			offset = 1;
		}
		else
			offset = 0;
			
		StringDelimiter tmp = null;
		token = token.substring(offset);
		
		for(StringDelimiter d : StringDelimiter.values())
		{
			if(token.startsWith(d.delimiter))
			{
				tmp = d;
				break;
			}
		}
		if(tmp == null)
		{
			throw new IllegalArgumentException("Unexpected error: "+token+
				" is not a valid string because it does not start with a valid delimiter.");
		}
		type.setInternal(tmp);
		
		buildTextNodes(node);
	}
	
	@Override
	protected void populateCompactUI(ActionNodePanel<LapNode> ui)
	{
		StringEditor editor = new StringEditor(value.get(), null);
		editor.setFont(Fonts.smallUI);
		editor.setForeground(new Color(0x0b4000));
		editor.addActionListener(new ActionListener()
		{
			//Note: need lis reference to live as long as the editor does, or the weakreference might be destroyed prematurely
			TypedListener<String> lis;
			{
				lis = new TypedListener<String>(){
					@Override
					public void valueChanged(
						Property<? extends String> property, String oldValue)
					{
						//Update editor to reflect new value
						editor.setValue(property.get());
					}
					
				};
				value.addListener(lis);
			}
			@Override
			public void actionPerformed(ActionEvent e)
			{
				try
				{
					//Update value from editor
					//Exclude lis to prevent unnecessary reacharound.
					value.change(editor.getValue(), lis, true);
				}
				catch (PropertyVetoException err)
				{
					//Reset value if it failed
					ui.rootPanel.userError(editor, err);
					editor.setValue(value.get());
				}
			}
		});
		ui.add(editor);
	}
	
	/*public LapNode getReplacement(LapNode newParent, String newValue)
	{
		return new LapStr(pyAST, newParent, type.get(), newValue);
	}*/
	
	@Override
	public String value()
	{
		return value.get();
	}
	/*
	@Override
	public void valueChanged(Property<? extends Object> property,
		Object oldValue)
	{
	}*/

	@Override
	public void vetoableChange(PropertyChangeEvent evt)
		throws PropertyVetoException
	{
		if(!isRegistered())
			return;
		String newValue;
		if(evt.getSource() == value)
			newValue = (String)evt.getNewValue();
		else
			newValue = value.get();
		
		StringDelimiter t;
		if(evt.getSource() == type)
			t = (StringDelimiter) evt.getNewValue();
		else
			t = type.get();
		boolean raw;
		if(evt.getSource() == rawFlag)
			raw = (boolean) evt.getNewValue();
		else
			raw = rawFlag.get();
		boolean unicorn;
		if(evt.getSource() == unicodeFlag)
			unicorn = (boolean) evt.getNewValue();
		else
			unicorn = unicodeFlag.get();
		
		String prefix = (raw?"r":"")+(unicorn?"u":"");
		
		//Str has the handy property that it is always a single token. The parent node must handle the rest.
		changeNodeText(prefix+t.delimiter+t.sanitize(newValue)+t.delimiter);
	}
}
